add api wxpay & add redis relative

Brightcells 9 years ago
parent
commit
7a17d0fb90

+ 8 - 1
api/urls.py

@@ -5,8 +5,9 @@ from django.conf.urls import url
5 5
 from account import views as account_views
6 6
 from group import views as group_views
7 7
 from message import views as message_views
8
-from photo import views as photo_views
9 8
 from operation import views as op_views
9
+from pay import views as pay_views
10
+from photo import views as photo_views
10 11
 
11 12
 
12 13
 # 帐户相关
@@ -69,3 +70,9 @@ urlpatterns += [
69 70
     url(r'^op/upgrade$', op_views.upgrade_api, name='upgrade_api'),  # APP 升级
70 71
     url(r'^op/splash$', op_views.splash_api, name='splash_api'),  # 启动页面
71 72
 ]
73
+
74
+# 支付相关
75
+urlpatterns += [
76
+    url(r'^order/create$', pay_views.order_create_api, name='order_create_api'),  # 订单创建
77
+    url(r'^order/notify_url$', pay_views.notify_url_api, name='notify_url_api'),  # 支付异步通知回调地址
78
+]

+ 3 - 0
group/views.py

@@ -25,6 +25,9 @@ import os
25 25
 import shortuuid
26 26
 
27 27
 
28
+r = settings.REDIS_CACHE
29
+
30
+
28 31
 @transaction.atomic
29 32
 def group_create_api(request):
30 33
     user_id = request.POST.get('user_id', '')

+ 11 - 0
pai2/func_settings.py

@@ -0,0 +1,11 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import redis
4
+
5
+
6
+def redis_connect(settings):
7
+    return redis.StrictRedis(**{
8
+        'host': settings.get('HOST', ''),
9
+        'port': settings.get('PORT', 0),
10
+        'password': '{user}:{pwd}'.format(settings.get('USER', ''), settings.get('PASSWORD', '')) if settings.get('USER', '') else ''
11
+    })

+ 43 - 2
pai2/settings.py

@@ -46,6 +46,7 @@ INSTALLED_APPS = (
46 46
     'group',
47 47
     'message',
48 48
     'operation',
49
+    'pay',
49 50
     'photo',
50 51
 )
51 52
 
@@ -158,6 +159,41 @@ REST_FRAMEWORK = {
158 159
     'PAGE_SIZE': 1
159 160
 }
160 161
 
162
+# Redis 设置
163
+REDIS = {
164
+    'default': {
165
+        'HOST': '127.0.0.1',
166
+        'PORT': 6379,
167
+        'USER': '',
168
+        'PASSWORD': ''
169
+    }
170
+}
171
+
172
+# Redis 缓存时间设置
173
+REDIS_EXPIRED_HOUR = 3600  # 60 * 60
174
+REDIS_EXPIRED_DAY = 86400  # 24 * 60 * 60
175
+REDIS_EXPIRED_WEEK = 604800  # 7 * 24 * 60 * 60
176
+REDIS_EXPIRED_MONTH = 2678400  # 31 * 24 * 60 * 60
177
+REDIS_EXPIRED_YEAR = 31622400  # 366 * 24 * 60 * 60
178
+
179
+# 微信设置
180
+WECHAT = {
181
+    'token': '5201314',
182
+    'appID': '',
183
+    'appsecret': '',
184
+    'mchID': '',
185
+    'apiKey': '',
186
+}
187
+
188
+WECHAT_GET_CODE = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect'
189
+WECHAT_GET_CODE_USERINFO = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect'
190
+WECHAT_GET_ACCESS_TOKEN = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code'
191
+
192
+WECHAT_GET_USERINFO = 'https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s'
193
+
194
+WXPAY_NOTIFY_SUCCESS = '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'
195
+WXPAY_NOTIFY_FAIL = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[XML PARSE FAIL]]></return_msg></xml>'
196
+
161 197
 # 唯一标识设置
162 198
 CURTAIL_UUID_LENGTH = 7
163 199
 
@@ -168,10 +204,9 @@ WATERMARK_LOGO = os.path.join(PROJ_DIR, 'static/pai2/img/paiai_96_96.png').repla
168 204
 THUMBNAIL_MAX_WIDTH = 360
169 205
 
170 206
 # 域名设置
171
-# DOMAIN = 'http://xfoto.com.cn'
172
-# IMG_DOMAIN = 'http://img.xfoto.com.cn'
173 207
 DOMAIN = 'http://pai.ai'
174 208
 IMG_DOMAIN = 'http://img.pai.ai'
209
+API_DOMAIN = 'http://api.pai.ai'
175 210
 
176 211
 # 消息图片设置
177 212
 PAI2_LOGO_URL = DOMAIN + '/static/pai2/img/paiai_96_96.png'
@@ -186,3 +221,9 @@ try:
186 221
     from local_settings import *
187 222
 except ImportError:
188 223
     pass
224
+
225
+try:
226
+    from func_settings import redis_connect
227
+    REDIS_CACHE = redis_connect(REDIS.get('default', {}))
228
+except ImportError:
229
+    REDIS_CACHE = None

+ 0 - 0
pay/__init__.py


+ 13 - 0
pay/admin.py

@@ -0,0 +1,13 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.contrib import admin
4
+
5
+from pay.models import OrderInfo
6
+
7
+
8
+class OrderInfoAdmin(admin.ModelAdmin):
9
+    list_display = ('order_id', 'from_uid', 'to_lid', 'to_uid', 'pay_status', 'paid_at', 'status', 'created_at', 'updated_at')
10
+    list_filter = ('pay_status', 'status')
11
+
12
+
13
+admin.site.register(OrderInfo, OrderInfoAdmin)

+ 35 - 0
pay/migrations/0001_initial.py

@@ -0,0 +1,35 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.db import models, migrations
5
+import shortuuidfield.fields
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+    ]
12
+
13
+    operations = [
14
+        migrations.CreateModel(
15
+            name='OrderInfo',
16
+            fields=[
17
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18
+                ('status', models.BooleanField(default=True, help_text='\u72b6\u6001', db_index=True, verbose_name='status')),
19
+                ('created_at', models.DateTimeField(help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at', auto_now_add=True)),
20
+                ('updated_at', models.DateTimeField(help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at', auto_now=True)),
21
+                ('order_id', shortuuidfield.fields.ShortUUIDField(help_text='\u8ba2\u5355\u552f\u4e00\u6807\u8bc6', max_length=22, editable=False, db_index=True, blank=True)),
22
+                ('from_uid', models.CharField(help_text='\u4ed8\u6b3e\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=255, verbose_name='from_uid', db_index=True)),
23
+                ('to_lid', models.CharField(max_length=255, blank=True, help_text='\u6536\u6b3e\u6444\u5f71\u5e08\u552f\u4e00\u6807\u8bc6', null=True, verbose_name='to_lid', db_index=True)),
24
+                ('to_uid', models.CharField(max_length=255, blank=True, help_text='\u6536\u6b3e\u7528\u6237\u552f\u4e00\u6807\u8bc6', null=True, verbose_name='to_uid', db_index=True)),
25
+                ('body', models.CharField(help_text='\u5546\u54c1\u63cf\u8ff0', max_length=255, null=True, verbose_name='body', blank=True)),
26
+                ('total_fee', models.IntegerField(default=0, help_text='\u603b\u91d1\u989d', verbose_name='total_fee')),
27
+                ('pay_status', models.IntegerField(default=0, help_text='\u652f\u4ed8\u72b6\u6001', db_index=True, verbose_name='pay_status', choices=[(0, '\u5f85\u652f\u4ed8'), (1, '\u5df2\u652f\u4ed8')])),
28
+                ('paid_at', models.DateTimeField(help_text='\u652f\u4ed8\u65f6\u95f4', null=True, verbose_name='paid_at', blank=True)),
29
+            ],
30
+            options={
31
+                'verbose_name': 'orderinfo',
32
+                'verbose_name_plural': 'orderinfo',
33
+            },
34
+        ),
35
+    ]

+ 0 - 0
pay/migrations/__init__.py


+ 52 - 0
pay/models.py

@@ -0,0 +1,52 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.conf import settings
4
+from django.db import models
5
+from django.utils.translation import ugettext_lazy as _
6
+
7
+from shortuuidfield import ShortUUIDField
8
+
9
+from pai2.basemodels import CreateUpdateMixin
10
+
11
+
12
+class OrderInfo(CreateUpdateMixin):
13
+    WAITING_PAY = 0
14
+    PAID = 1
15
+    # DELETED = 2
16
+
17
+    PAY_STATUS = (
18
+        (WAITING_PAY, u'待支付'),
19
+        (PAID, u'已支付'),
20
+        # (DELETED, u'已删除'),
21
+    )
22
+
23
+    order_id = ShortUUIDField(_(u'order_id'), max_length=255, help_text=u'订单唯一标识', db_index=True)
24
+
25
+    from_uid = models.CharField(_(u'from_uid'), max_length=255, help_text=u'付款用户唯一标识', db_index=True)
26
+    to_lid = models.CharField(_(u'to_lid'), max_length=255, blank=True, null=True, help_text=u'收款摄影师唯一标识', db_index=True)
27
+    to_uid = models.CharField(_(u'to_uid'), max_length=255, blank=True, null=True, help_text=u'收款用户唯一标识', db_index=True)
28
+
29
+    body = models.CharField(_(u'body'), max_length=255, blank=True, null=True, help_text=u'商品描述')
30
+    total_fee = models.IntegerField(_(u'total_fee'), default=0, help_text=u'总金额')
31
+
32
+    pay_status = models.IntegerField(_(u'pay_status'), choices=PAY_STATUS, default=WAITING_PAY, help_text=u'支付状态', db_index=True)
33
+    paid_at = models.DateTimeField(_(u'paid_at'), blank=True, null=True, help_text=_(u'支付时间'))
34
+
35
+    class Meta:
36
+        verbose_name = _('orderinfo')
37
+        verbose_name_plural = _('orderinfo')
38
+
39
+    def __unicode__(self):
40
+        return u'{0.pk}'.format(self)
41
+
42
+    @property
43
+    def data(self):
44
+        return {
45
+            'order_id': self.order_id,
46
+            'from_uid': self.from_uid,
47
+            'to_lid': self.to_lid,
48
+            'to_uid': self.to_uid,
49
+            'pay_status': self.pay_status,
50
+            'paid_at': self.paid_at,
51
+            'created_at': self.created_at,
52
+        }

+ 3 - 0
pay/tests.py

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 94 - 0
pay/views.py

@@ -0,0 +1,94 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.conf import settings
4
+from django.db import transaction
5
+from django.http import JsonResponse
6
+from django.shortcuts import HttpResponse
7
+
8
+from pay.models import OrderInfo
9
+
10
+from utils.errno_utils import OrderStatusCode
11
+from utils.response_utils import response
12
+
13
+from TimeConvert import TimeConvert as tc
14
+from wechatpy import WeChatPay, WeChatPayException
15
+
16
+import xmltodict
17
+
18
+WECHAT = settings.WECHAT
19
+
20
+wxpay = WeChatPay(WECHAT['appID'], WECHAT['apiKey'], WECHAT['mchID'])
21
+
22
+
23
+@transaction.atomic
24
+def order_create_api(request):
25
+    from_uid = request.POST.get('from_uid', '')
26
+    to_lid = request.POST.get('to_lid', '')
27
+    to_uid = request.POST.get('to_uid', '')
28
+
29
+    body = request.POST.get('body', '')  # 商品描述
30
+    total_fee = int(request.POST.get('total_fee', 0))  # 总金额,单位分
31
+
32
+    # JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付,统一下单接口trade_type的传参可参考这里
33
+    trade_type = request.POST.get('trade_type', '')
34
+
35
+    # 生成订单
36
+    order = OrderInfo.objects.create(from_uid=from_uid, to_lid=to_lid, to_uid=to_uid, total_fee=total_fee)
37
+
38
+    try:
39
+        prepay_data = wxpay.order.create(
40
+            body=body,
41
+            notify_url=settings.API_DOMAIN + '/order/notify_url',
42
+            out_trade_no=order.order_id,
43
+            total_fee=total_fee,
44
+            trade_type=trade_type,
45
+            # user_id=None,  # 可选,用户在商户appid下的唯一标识。trade_type=JSAPI,此参数必传
46
+        )
47
+    except WeChatPayException:
48
+        return response(OrderStatusCode.WX_UNIFIED_ORDER_FAIL)
49
+
50
+    prepay_id = prepay_data.get('prepay_id', '')
51
+    wxpay_params = wxpay.jsapi.get_jsapi_params(prepay_id)
52
+
53
+    return JsonResponse({
54
+        'status': 200,
55
+        'data': {
56
+            'order_id': order.order_id,
57
+            'prepay_id': prepay_id,
58
+            'wxpay_params': wxpay_params,
59
+        }
60
+    })
61
+
62
+
63
+def order_paid_success(order):
64
+    if order.pay_status == OrderInfo.PAID:
65
+        return
66
+
67
+    order.pay_status = OrderInfo.PAID
68
+    order.paid_at = tc.utc_datetime()
69
+    order.save()
70
+
71
+
72
+@transaction.atomic
73
+def notify_url_api(request):
74
+    try:
75
+        data = xmltodict.parse(request.body)['xml']
76
+    except xmltodict.ParsingInterrupted:
77
+        # 解析 XML 失败
78
+        return HttpResponse(settings.WXPAY_NOTIFY_FAIL)
79
+
80
+    out_trade_no = data.get('out_trade_no', '')
81
+    return_code = data.get('return_code', '')
82
+    result_code = data.get('result_code', '')
83
+
84
+    if return_code != 'SUCCESS' or result_code != 'SUCCESS':
85
+        return HttpResponse(settings.WXPAY_NOTIFY_FAIL)
86
+
87
+    try:
88
+        order = OrderInfo.objects.get(order=out_trade_no)
89
+    except OrderInfo.DoesNotExist:
90
+        return HttpResponse(settings.WXPAY_NOTIFY_FAIL)
91
+
92
+    order_paid_success(order)
93
+
94
+    return HttpResponse(settings.WXPAY_NOTIFY_SUCCESS)

+ 4 - 5
photo/models.py

@@ -63,7 +63,8 @@ class PhotosInfo(CreateUpdateMixin):
63 63
     def r_photo_url(self):
64 64
         return u'{0}/{1}'.format(settings.IMG_DOMAIN, self.r_photo_path) if self.r_photo_path else ''
65 65
 
66
-    def _data(self):
66
+    @property
67
+    def data(self):
67 68
         return {
68 69
             'pk': self.pk,
69 70
             'user': self.lensman_id,
@@ -71,7 +72,8 @@ class PhotosInfo(CreateUpdateMixin):
71 72
             'photo': self.photo_id,
72 73
         }
73 74
 
74
-    def _detail(self):
75
+    @property
76
+    def detail(self):
75 77
         return {
76 78
             'pk': self.pk,
77 79
             'user': self.lensman_id,
@@ -79,6 +81,3 @@ class PhotosInfo(CreateUpdateMixin):
79 81
             'photo': self.photo_id,
80 82
             'photo_url': self.p_photo_url,
81 83
         }
82
-
83
-    data = property(_data)
84
-    detail = property(_detail)

+ 3 - 0
requirements.txt

@@ -2,6 +2,7 @@ CodeConvert==2.0.4
2 2
 Django==1.8.4
3 3
 MySQL-python==1.2.5
4 4
 TimeConvert==1.1.6
5
+cryptography==1.2.1
5 6
 django-curtail-uuid==1.0.0
6 7
 django-multidomain==1.1.4
7 8
 django-shortuuidfield==0.1.3
@@ -12,5 +13,7 @@ kkconst==1.1.2
12 13
 pep8==1.6.2
13 14
 pillow==2.9.0
14 15
 pytz==2015.7
16
+redis==2.10.5
15 17
 shortuuid==0.4.2
16 18
 uWSGI==2.0.11.1
19
+wechatpy==1.2.5

+ 14 - 4
utils/errno_utils.py

@@ -15,6 +15,7 @@ class StatusCodeField(ConstIntField):
15 15
 
16 16
 
17 17
 class UserStatusCode(BaseStatusCode):
18
+    """ 摄影师/用户相关错误码 400x & 401x """
18 19
     LENSMAN_NOT_FOUND = StatusCodeField(4000, u'Lensman Not Found', description=u'摄影师不存在')
19 20
     LENSMAN_PASSWORD_ERROR = StatusCodeField(4001, u'Lensman Password Error', description=u'摄影师密码错误')
20 21
     USERNAME_HAS_REGISTERED = StatusCodeField(4010, u'Username Has Registered', description=u'用户名已注册')
@@ -23,27 +24,36 @@ class UserStatusCode(BaseStatusCode):
23 24
 
24 25
 
25 26
 class PhotoStatusCode(BaseStatusCode):
27
+    """ 照片相关错误码 403x """
26 28
     PARAMS_ERROR = StatusCodeField(4039, u'Params Error', description=u'参数错误')
27 29
 
28 30
 
29 31
 class GroupStatusCode(BaseStatusCode):
32
+    """ 群组相关错误码 402x """
30 33
     GROUP_NOT_FOUND = StatusCodeField(4020, u'Group Not Found', description=u'群组不存在')
31 34
     GROUP_HAS_LOCKED = StatusCodeField(4021, u'Group Has Locked', description=u'群组已锁定')
32 35
     NOT_GROUP_ADMIN = StatusCodeField(4022, u'Not Group Admin', description=u'非群组管理员')
33 36
     NO_UPDATE_PERMISSION = StatusCodeField(40220, u'No Update Permission', description=u'没有更新权限')
34 37
     NO_LOCK_PERMISSION = StatusCodeField(40221, u'No Lock Permission', description=u'没有锁定权限')
35
-    NO_UNLOCK_PERMISSION = StatusCodeField(40221, u'No Unlock Permission', description=u'没有解锁权限')
36
-    NO_REMOVE_PERMISSION = StatusCodeField(40222, u'No Remove Permission', description=u'没有移除权限')
37
-    NO_PASS_PERMISSION = StatusCodeField(40223, u'No Pass Permission', description=u'没有通过权限')
38
-    NO_REFUSE_PERMISSION = StatusCodeField(40224, u'No Refuse Permission', description=u'没有拒绝权限')
38
+    NO_UNLOCK_PERMISSION = StatusCodeField(40222, u'No Unlock Permission', description=u'没有解锁权限')
39
+    NO_REMOVE_PERMISSION = StatusCodeField(40223, u'No Remove Permission', description=u'没有移除权限')
40
+    NO_PASS_PERMISSION = StatusCodeField(40224, u'No Pass Permission', description=u'没有通过权限')
41
+    NO_REFUSE_PERMISSION = StatusCodeField(40225, u'No Refuse Permission', description=u'没有拒绝权限')
39 42
     DUPLICATE_JOIN_REQUEST = StatusCodeField(4027, u'Duplicate Join Request', description=u'重复加群申请')
40 43
     JOIN_REQUEST_NOT_FOUND = StatusCodeField(4028, u'Join Request Not Found', description=u'加群申请不存在')
41 44
     GROUP_USER_NOT_FOUND = StatusCodeField(4029, u'Group User Not Found', description=u'该用户不在群组')
42 45
 
43 46
 
44 47
 class GroupPhotoStatusCode(BaseStatusCode):
48
+    """ 飞图相关错误码 403x """
45 49
     GROUP_PHOTO_NOT_FOUND = StatusCodeField(4030, u'Group Photo Not Found', description=u'飞图不存在')
46 50
 
47 51
 
52
+class OrderStatusCode(BaseStatusCode):
53
+    """ 订单/支付相关错误码 404x """
54
+    WX_UNIFIED_ORDER_FAIL = StatusCodeField(4040, u'WX Unified Order Fail', description=u'微信统一下单失败')
55
+
56
+
48 57
 class MessageStatusCode(BaseStatusCode):
58
+    """ 消息相关错误码 409x """
49 59
     MESSAGE_NOT_FOUND = StatusCodeField(4091, u'Message Not Found', description=u'消息不存在')

+ 0 - 0
utils/redis/__init__.py


+ 0 - 0
utils/redis/keys.py